
/**
 * Universidad del Valle de Guatemala
 * CC2003 Algoritmos y Estructuras de Datos
 * Proyecto 1: Intérprete de LISP --> Lambda LISP
 * @version 2 Septiembre, 2009
 * Integrantes:
 *      @author Julio Chicas, José García, Ernesto Rodríguez, Karen Tojín
 **/
/**
 * Descripción: Clase que realiza las evaluaciones de los inputs ingresados para
 * definir que operación se va a realizar y realizar la operación
 **/
import java.util.List;

public class Operations {

    /**
     * Método que revisa que operación se llevó a cabo y devuelve el resultado
     * @param messaje: Lo ingresado por el usuario
     * @param var:  Las variables con las que se está trabajando
     * @return El resultado de la operación realizada
     * @throws UnoperableException
     */
    public static Object operate(Object messaje, VarArray var) throws UnoperableException {

	List operator = null;

        try {
            messaje = ((String) messaje).toUpperCase();
            operator = Parser.parse((String) messaje);
        } catch (Exception e) {
            try {
                operator = (List) messaje;
            } catch (ClassCastException b) {
                try {
                    return var.retVal((String) messaje);
                } catch (Exception c) {
                    if (messaje.getClass().equals((new Float(5)).getClass())) {
                        return messaje;
                    } else {

                        try {
                            return (Float.parseFloat((String) messaje));
                        } catch (NumberFormatException g) {
                            try {
                                if (((String) messaje).substring(0, 1).equals("'")) {
                                    try {
                                        if (!(((String) messaje).substring(1, 2).equals("("))) {
                                            return ((String) messaje).substring(1);
                                        }
                                    } catch (Exception h) {
                                        return ((String) messaje).substring(1);
                                    }
                                } else {
                                    if (((String) messaje).equals("NIL") || ((String) messaje).equals("T")) {
                                        return messaje;
                                    }
                                }

                            } catch (Exception f) {
                            }

                        }
                    }
                }
            }
        }
	if(operator==null){
	    throw new UnoperableException();
	}
        if (operator.size() == 0) {
            return "NIL";
        }
        /**
         * En esta sección se hancen las evaluaciones de las operaciones ingresadas
         */
        try {
            String op = (String) (operator.get(0));
            if (op.equals("+") || op.equals("*") || op.equals("-") || op.equals("/") || op.equals("MOD")) {
                return (operacionesAritmeticas(operator, var));
            }
            if (op.equals("AND") || op.equals("OR") || op.equals("NOT") || op.equals("ATOM") || op.equals("EQUAL") || op.equals("=") || op.equals("/=") || op.equals("<=") || op.equals(">=") || op.equals("<") || op.equals(">")) {
                return (opBoolean(operator, var));
            }
            if (op.equals("CAR")) {
                return CAR(operator, var);
            }
            if (op.equals("CDR")) {
                return CDR(operator, var);
            }
            if (op.equals("COND")) {
                return operateCond(operator, var);
            }
            if (op.equals("IF")) {
                return operateIf(operator, var);
            }
            if (op.equals("SETQ")) {
                return setQ(operator, var);
            }
            if (op.equals("'")) {
                return apostrofe(operator);
            }
            if (op.equals("DEFUN")) {
                return defun(messaje.toString(), var);
            }
            if (op.equals("PRINT")) {
                return print(operator, var);
            }

            if (op.equals("LIST")) {
                return list(operator, var);
            }
            if (op.equals("PROG1")) {
                return prog1(operator, var);
            }
            if (op.equals("PROGN")) {
                return progN(operator, var);
            }
            if (op.equals("MAPCAR")) {
                return mapCar(operator, var);
            }
            if (op.equals("APPEND")) {
                return append(operator, var);
            }

            return Stored.evalFun(operator, var);
        } catch (ClassCastException e) {/////la excepción que debe ser capturable es la de casting, cualquier otra debe de devolver el error
        }

        throw new UnoperableException();
    }

    /**
     * Metodo que raliza la operación MAPCAR
     * @param operator: El oeprador de la expresion
     * @param var:  Las variables con las que se está trabajando
     * @return Una lista con el resultado de la operación
     * @throws UnoperableException
     */
    private static Object mapCar(List operator, VarArray var) throws UnoperableException {
        if (operator.size() != 3) {
            throw new UnoperableMapCarException();
        }
        String function = "";
        try {
            function = ((String) operate(operator.get(1), var));
        } catch (ClassCastException e) {
            throw new UnoperableMapCarException();
        }
        Object result = operate(operator.get(2), var);
        List op = null;
        try {
            if (((String) result).equals("NIL")) {
                return "NIL";
            }
        } catch (ClassCastException e) {
            op = (List) result;
        }
        List res = new Lista();
        String expression = "";
        try {
            for (int a = 0; a < op.size(); a++) {
                expression = "( " + function + " " + op.get(a).toString() + " )";
                result = operate(expression, var);
                res.add(result);
            }
            return res;

        } catch (Exception e) {
        }
        try {
            for (int a = 0; a < op.size(); a++) {
                expression = "( " + function + " '" + op.get(a).toString() + " )";
                result = operate(expression, var);
                res.add(result);
            }
            return res;
        } catch (Exception e) {
            throw new UnoperableMapCarException();
        }
    }

    /**
     * Método que realiza la operación de PROG1
     * @param operator: El operador
     * @param var: Los operandos
     * @return Un objeto con el resultado de la operación, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object prog1(List operator, VarArray var) throws UnoperableException {
        if (operator.size() == 1) {
            throw new UnoperableProg1Exception();
        }
        Object res = null;
        res = operate(operator.get(1), var);
        for (int a = 2; a < operator.size(); a++) {
            operate(operator.get(a), var);
        }
        return res;
    }

    /**
     * Método que realiza la operación de PROGN
     * @param operator: El operador
     * @param var:  Las variables con las que se está trabajando
     * @return Un objeto con el resultado de la operación, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object progN(List operator, VarArray var) throws UnoperableException {
        if (operator.size() == 1) {
            return "NIL";
        }
        Object res = null;
        for (int a = 1; a < operator.size(); a++) {
            res = operate(operator.get(a), var);
        }
        return res;
    }

    /**
     * Método que realiza la operación de LIST
     * @param operator: El operador
     * @param var:  Las variables con las que se está trabajando
     * @return Un objeto con el resultado de la operación, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object list(List operator, VarArray var) throws UnoperableException {
        if (operator.size() == 1) {
            return "NIL";
        }
        Object add = null;
        List res = new Lista();
        for (int a = 1; a < operator.size(); a++) {
            add = operate(operator.get(a), var);
            res.add(add);
        }
        return res;
    }

    /**
     * Método que devuelve un String, que se utilizará para la definición de funciones
     * @param op: La función definida
     * @return Un string con la función que se va a realizar
     */
    private static String prepareDefun(String op) {
        String res = "";
        String f = "";
        for (int a = 0; a < op.length() - 1; a++) {
            f = op.substring(a, a + 2);
            if (f.equals("('")) {
                res += "'(";
                a++;
            } else {
                res += f.charAt(0);
            }
        }
        res += ")";
        return res;
    }

    /**
     * Método que realiza la definción de funciones
     * @param operator: La función a realizar
     * @return La función definida
	 *@param var: Las variables con las que se está trabajando
     * @throws UndefinableFunctionException
     */
    private static String defun(String operator,VarArray var) throws UndefinableFunctionException {
        operator = prepareDefun(operator);
        try {
            List<String> op = Parser.simpleParse(operator);
            String name = "";
            op.remove(0);
            name = undefinable(op.remove(0));
	    List<String>operands=Parser.simpleParse(op.remove(0));

            if (op.size() > 0) {

                Stored.addFun(name, operands, prepare(operands,op, var));
                return name;
            }
        } catch (Exception e) {
        }
        throw new UndefinableFunctionException();
    }
    /**
     * Método para la definición de variables con la isntrucción SETQ
     * @param operator: El operador
     * @param var: Los operandos
     * @return La variable con el valor asignado, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object setQ(List operator, VarArray var) throws UnoperableException {
        int size = operator.size();
        if (size % 2 == 1 && size != 1) {
            String name = "";
            Object res = null;
            for (int a = 0; 2 * a < size - 2; a++) {
                try {
                    name = undefinable((String) operator.get(2 * a + 1));

                } catch (Exception e) {
                    throw new UnoperableSetQException();
                }
                res = operate(operator.get(2 * a + 2), var);
                Stored.addVar(name, res);
                if (var != Stored.getVar()) {
                    var.add(name, res);
                }

            }
            return res;
        }
        throw new UnoperableSetQException();
    }

    /**
     * Método que evalúa si el nombre de una función es posible de definir o no,
     * es decir si es alguna palabra reservada de lisp o algún símbolo
     * @param c: El posible nombre de la función
     * @return El nombre de la función si sí puede ser definido
     * @throws UndefinableNameException
     */
    private static String undefinable(String c) throws UndefinableNameException {
        if (c.equals("")) {
            throw new UndefinableNameException();
        }
        String forbidden = "\";)(,";
        boolean incorrect = false;
        for (int a = 0; a < c.length(); a++) {
            incorrect = incorrect && c.contains(forbidden.substring(a, a + 1));
        }
        try {
            Float.parseFloat(c);
            incorrect = true;
        } catch (NumberFormatException e) {
            if (c.charAt(0) == '+' || c.charAt(0) == '-') {
                incorrect = true;
            }
        }

        if (incorrect || c.equals("AND") || c.equals("OR") || c.equals("NOT") || c.equals("EQUAL") ||
                c.equals("MOD") || c.equals("ATOM") || c.equals("T") || c.equals("NIL") || c.equals("CAR") ||
                c.equals("COND") || c.equals("CDR") || c.equals("IF") || c.equals("DEFUN") || c.equals("SETQ") ||
                c.equals("LIST") || c.equals("MAPCAR") || c.equals("PROG1") || c.equals("PROGN") || c.equals("PRINT") ||
                c.equals("*") || c.equals("/") || c.equals("<") || c.equals(">") || c.equals("=") ||
                c.equals(">=") || c.equals("<=") || c.equals("/=") || c.equals(".") || c.equals("CONS")) {
            throw new UndefinableNameException();
        }
        return c;
    }

    /**
     * Método que realiza el condicional COND
     * @param operator: El operador
     * @param var: Las variables con las que se está trabajando
     * @return El resultado de la operación COND, el caso verdadero o el default
     * puede una lista o un elemento
     * @throws UnoperableException
     */
    private static Object operateCond(List operator, VarArray var) throws UnoperableException {
        if (operator.size() < 2) {
            throw new UnoperableIfException();
        }
        List cases;
        boolean condition = false;
        try {
            for (int a = 1; a < operator.size(); a++) {
                cases = (List) operator.get(a);
                try {
                    condition = unBool((String) cases.get(0));
                } catch (Exception e) {
                    condition = unBool(operate(cases.get(0), var));
                }
                if (condition) {
                    return operate(cases.get(1), var);
                }
            }
        } catch (ClassCastException e) {
            throw new UnoperableIfException();
        }
        return "NIL";

    }

    /**
     * Método que realiza el condicional IF
     * @param operators: Los operadores
     * @param var:  Las variables con las que se está trabajando
     * @return El resultado del IF, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object operateIf(List operators, VarArray var) throws UnoperableException {
        if (operators.size() == 4) {
            boolean cond;
            try {
                cond = unBool((String) operators.get(1));
            } catch (Exception e) {
                cond = unBool(operate(operators.get(1), var));
            }
            if (cond) {
                return operate(operators.get(2), var);

            } else {
                return operate(operators.get(3), var);
            }
        }
        throw new UnoperableIfException();
    }

    /**
     * Método que realiza las operaciones aritméticas +,*,-,/,Mod
     * @param operation: La operación a realizar
     * @param var: Las variables con las que se está trabajando
     * @return El resultado de la operación realizada
     * @throws UnoperableArithmeticException
     */
    private static Float operacionesAritmeticas(List operation, VarArray var) throws UnoperableArithmeticException {

        int size = operation.size();
        if (size < 2) {
            throw new UnoperableArithmeticException();
        }
        float res;
        char operand = ((String) operation.get(0)).charAt(0);
        try {
            switch (operand) {
                case '+':
                    res = 0;
                    for (int a = 1; a < size; a++) {
                        res += ((Float) operate(operation.get(a), var));
                    }
                    return res;

                case '*':
                    res = 1;
                    for (int a = 1; a < size; a++) {
                        res *= ((Float) operate(operation.get(a), var));
                    }
                    return res;

                case '-':
                    res = 0;
                    if (size == 2) {
                        res -= ((Float) operate(operation.get(1), var));
                        return res;
                    } else {
                        if (size == 3) {
                            res = ((Float) operate(operation.get(1), var)) - ((Float) operate(operation.get(2), var));
                            return res;
                        } else {
                            throw new UnoperableArithmeticException();
                        }
                    }

                case '/': {
                    if (size == 3) {
                        res = ((Float) operate(operation.get(1), var)) / ((Float) operate(operation.get(2), var));
                        return res;
                    } else {
                        throw new UnoperableArithmeticException();
                    }
                }

                case 'M':
                    res = 1;

                    if (size == 3) {
                        res = new Float((int) ((Float) operate(operation.get(1), var)).floatValue() % (int) ((Float) operate(operation.get(2), var)).floatValue());
                        return res;
                    } else {
                        throw new UnoperableArithmeticException();
                    }

            }
        } catch (Exception e) {
            throw new UnoperableArithmeticException();
        }
        return null;
    }

    /**
     * Método que cambia los true y false de java a T y NIL de Lisp
     * @param b: El booleano a cambiar
     * @return El String cambiado
     */
    private static String prepBool(Boolean b) {
        return ((b) ? "T" : "NIL");
    }

    /**
     * Método que cambia de NIL y T a true y false
     * @param m: String a cambiar
     * @return Booleano cambiado
     */
    private static Boolean unBool(Object m) {
        try {
            String t = (String) m;
            if (t.equals("NIL")) {
                return false;
            }
        } catch (ClassCastException e) {
        }
        return true;
    }

    /**
     * Método que realiza las operaciones booleanas >,<,=,/=, EQUAL, >=, <=, AND, OR, NOT
     * @param operation: La operación a realizar
     * @param var:  Las variables con las que se está trabajando
     * @return El resultado de la operación booleana realizada, T o NIL
     * @throws UnoperableBooleanException
     */
    private static String opBoolean(List operation, VarArray var) throws UnoperableBooleanException {
        int size = operation.size();
        if (size < 2) {
            throw new UnoperableBooleanException();
        }
        boolean res;
        char operand = ((String) operation.get(0)).charAt(0);
        try {
            switch (operand) {
                case 'O':
                    res = false;
                    for (int a = 1; a < size; a++) {
                        res = res || unBool((operate(operation.get(a), var)));
                    }
                    return prepBool(res);

                case 'A':
                    if (((String) operation.get(0)).equals("AND")) {
                        res = true;
                        for (int a = 1; a < size; a++) {
                            res = res && unBool((operate(operation.get(a), var)));
                        }
                        return prepBool(res);
                    } else {
                        if (operation.size() == 2) {
                            try {
                                ((List) operate(operation.get(1), var)).get(0);
                                return "NIL";
                            } catch (Exception e) {
                                return "T";
                            }
                        }

                    }
                case 'N':
                    if (size == 2) {
                        res = !unBool((operate(operation.get(1), var)));
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }

                case '/':
                    if (size == 3) {
                        res = (((Float) operate(operation.get(1), var))).floatValue() != ((Float) (operate(operation.get(2), var))).floatValue();
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }

                case '=':
                    if (size == 3) {
                        res = (((Float) operate(operation.get(1), var))).floatValue() == ((Float) (operate(operation.get(2), var))).floatValue();
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }
                case 'E':
                    if (size == 3) {
                        res = operate(operation.get(1), var).toString().equals(operate(operation.get(2), var).toString());
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }
                case '<':
                    if (size == 3) {
                        if (((String) operation.get(0)).equals("<=")) {
                            res = (((Float) operate(operation.get(1), var))).floatValue() <= ((Float) (operate(operation.get(2), var))).floatValue();
                        } else {
                            res = (((Float) operate(operation.get(1), var))).floatValue() < ((Float) (operate(operation.get(2), var))).floatValue();
                        }
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }
                case '>':
                    if (size == 3) {
                        if (((String) operation.get(0)).equals(">=")) {
                            res = (((Float) operate(operation.get(1), var))).floatValue() >= ((Float) (operate(operation.get(2), var))).floatValue();
                        } else {
                            res = (((Float) operate(operation.get(1), var))).floatValue() > ((Float) (operate(operation.get(2), var))).floatValue();
                        }
                        return prepBool(res);
                    } else {
                        throw new UnoperableBooleanException();
                    }
                default:
                    throw new UnoperableBooleanException();
            }
        } catch (Exception e) {
            throw new UnoperableBooleanException();
        }

    }

    /**
     * Método que realiza la operación CAR
     * @param inputSend: El string ingresado
     * @param var:  Las variables con las que se está trabajando
     * @return El primer elemento de la lista enviada, puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object CAR(List inputSend, VarArray var) throws UnoperableException {
        int size = inputSend.size();
        if (size != 2) {
            throw new UnoperableCARException();
        }
        List CARList = new Lista();
        Object imput = operate(inputSend.get(1), var);
        try {
            CARList = (List) imput;
            if (CARList.size() == 0) {
                return "NIL";
            }
        } catch (ClassCastException e) {
            try {
                if (((String) imput).equals("NIL")) {
                    return "NIL";
                } else {
                    throw new UnoperableCARException();
                }
            } catch (ClassCastException f) {
                throw new UnoperableCARException();
            }

        }
        return CARList.get(0);
    }

    /**
     * Método que realiza la operación CDR
     * @param inputSend: El string ingresado
     * @param var: Las variables con las que se está trabajando
     * @return Todos los elementos de una lista enviada excepto el primero,
     * puede ser una lista o un elemento
     * @throws UnoperableException
     */
    private static Object CDR(List inputSend, VarArray var) throws UnoperableException {
        int size = inputSend.size();
        if (size != 2) {
            throw new UnoperableCDRException();
        }
        List CDRList = new Lista();
        Object imput = operate(inputSend.get(1), var);
        try {
            CDRList = (List) imput;
        } catch (ClassCastException e) {
            try {
                if (((String) imput).equals("NIL")) {
                    return "NIL";
                } else {
                    throw new UnoperableCDRException();
                }
            } catch (ClassCastException f) {
                throw new UnoperableCDRException();
            }
        }
        try {
            List returnable = new Lista();
            CDRList.get(1);
            for (int a = 1; a < CDRList.size(); a++) {
                returnable.add(CDRList.get(a));
            }
            return returnable;
        } catch (Exception e) {
            return "NIL";
        }
    }

    /**
     * Método que realiza la evaluación del apóstrofe
     * @param operators: Lista con un aóstrofe como primer elemento
     * @return Una lista a la que se le quitó el apóstrofe como primer elemento
     */
    private static Object apostrofe(List operators) {
        List operator = new Lista();
	List listed;
        for (int a = 1; a < operators.size(); a++) {
	    try{
		listed=(List)operators.get(a);
		if(listed.size()!=0){
		    operator.add(operators.get(a));
		}else{
		    operator.add("NIL");
		}
	    }catch(ClassCastException e){
		operator.add(operators.get(a));
	    }
        }
        if (operator.size() == 0) {
            return "NIL";
        }
        return operator;
    }

    /**
     * Método que realiza la operación PRINT
     * @param operator: El operador
     * @param var:  Las variables con las que se está trabajando
     * @return El elemento a imprimir
     * @throws UnoperableException
     */
    private static Object print(List operator, VarArray var) throws UnoperableException {
        if (operator.size() == 2) {
            Object res = operate(operator.get(1), var);
            System.out.println(res.toString());
            return res;
        }
        throw new PrintingException();
    }

    /**
     * Método que realiza la operación APPEND
     * @param operator: El operador
     * @param var:  Las variables con las que se está trabajando
     * @return El resultado del append
     * @throws UnoperableException
     */
    private static Object append(List operator, VarArray var) throws UnoperableException {
        if (operator.size() > 2) {
            List list1 = new Lista();
            List res = new Lista();
            Object op = null;
            for (int a = 1; a < operator.size(); a++) {
                op = operate(operator.get(a), var);

                try {
                    list1 = (Lista) op;
                } catch (ClassCastException e) {
                    try {
                        if (!((String) op).equals("NIL")) {
                            throw new AppendException();
                        }
                    } catch (ClassCastException f) {
                        throw new AppendException();
                    }
                }
                for (int b = 0; b < list1.size(); b++) {
                    res.add(list1.get(b));
                }
                list1 = new Lista();
            }


            if (res.size() == 0) {
                return "NIL";
            }
            return res;
        }
        throw new AppendException();
    }
	
	/**
     * Método que realiza ayudar a la funcion "DEFUN"
     * @param operands: Los operandos (t no debe ser igual a uno de estos)
     * @param t: El string que se quiere revisar que no sea igual a uno de los operandos. 
     * @return Veradero si "t" no es igual a uno de los operandos, Falso de lo contrario.
     */
	private static boolean finder(List<String> operands, String r){
	boolean res=true;
	    for(int a=0; a<operands.size(); a++){
		res=res&&(!r.equals(operands.get(a)));
	    }
	
	return res;
    }
	/**
     * Método que ayuda a "DEFUN" haciendo las sustituciones necesarias de variables no globales por sus valores.
     * @param operands: Los operandos, de estar incluidos, estos no puede ser sustituidos
     * @param var: Las variables con las que se cuenta
     * @param operations:  Las operaciones, en ellas se deberán sustituir los valores necesarios.
     * @return La lista de Strings con operaciones, ahora con las modificaciones realizadas
     * @throws UdefinableFunctionException
     */
    private static List<String> prepare(List<String> operands, List<String>operations,VarArray var)throws UndefinableFunctionException{
	List<String> res=new Lista<String>();
	List preres=new Lista();
	List postres=new Lista();
	String segment="";
	boolean continues=true;
	try{

	    for (int a=0; a< operations.size(); a++){
		try{
		    preres=Parser.parse(operations.get(a));

		}catch(Exception e){

		    continues=false;
		    res.add(operations.get(a));
		}
		if(continues){
		    for(int b=0; b<preres.size();b++){
			try{
			    segment=(String)preres.get(b);
			    if(finder(operands, segment)){
				try{
				    postres.add(var.retVal2(segment));
				}catch(NonexistentException e){
				    postres.add(segment);
				}

			    }else{
				postres.add(segment);
			    }
			}catch(ClassCastException e){
			    postres.add(preres.get(b));
			}

			segment="";
		    }
		    res.add(prepareDefun(postres.toString()));
		    postres=new Lista();
		}else{
		    continues=true;
		}
		preres=new Lista();
	    }


	}catch(Exception e) {
	    throw new UndefinableFunctionException();
	}

	return res;
    }
	
	
}
